<script>
import {
  GlAlert,
  GlCard,
  GlButton,
  GlButtonGroup,
  GlFormTextarea,
  GlModal,
  GlTooltipDirective,
  GlSkeletonLoader,
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import download from '~/lib/utils/downloader';
import { visitUrl } from '~/lib/utils/url_utility';
import { fetchPolicies } from '~/lib/graphql';
import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card_graphql.vue';
import IssueNote from 'ee/vue_shared/security_reports/components/issue_note_graphql.vue';
import MergeRequestNote from 'ee/vue_shared/security_reports/components/merge_request_note_graphql.vue';
import VulnerabilityDetailsGraphql from 'ee/security_dashboard/components/shared/vulnerability_details_graphql/index.vue';
import DismissalNote from 'ee/vue_shared/security_reports/components/dismissal_note.vue';
import SplitButton from 'ee/vue_shared/security_reports/components/split_button.vue';
import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue';
import securityReportFindingQuery from 'ee/security_dashboard/graphql/queries/security_report_finding.query.graphql';
import dismissFindingMutation from 'ee/security_dashboard/graphql/mutations/dismiss_finding.mutation.graphql';
import createMergeRequestMutation from 'ee/security_dashboard/graphql/mutations/finding_create_merge_request.mutation.graphql';
import revertFindingToDetectedMutation from 'ee/security_dashboard/graphql/mutations/revert_finding_to_detected.mutation.graphql';
import createIssueMutation from 'ee/security_dashboard/graphql/mutations/finding_create_issue.mutation.graphql';
import { updateFindingState } from 'ee/security_dashboard/graphql/cache_updates';
import { VULNERABILITY_MODAL_ID } from 'ee/vue_shared/security_reports/components/constants';

export const STATE_DISMISSED = 'DISMISSED';
export const STATE_DETECTED = 'DETECTED';
export const STATE_RESOLVED = 'RESOLVED';

export default {
  components: {
    DismissalNote,
    EventItem,
    GlAlert,
    GlCard,
    GlButton,
    GlButtonGroup,
    GlFormTextarea,
    GlModal,
    GlSkeletonLoader,
    IssueNote,
    MergeRequestNote,
    SolutionCard,
    SplitButton,
    VulnerabilityDetailsGraphql,
  },
  directives: {
    GlTooltip: GlTooltipDirective,
  },
  props: {
    findingUuid: {
      type: String,
      required: true,
    },
    pipelineIid: {
      type: Number,
      required: true,
    },
    projectFullPath: {
      type: String,
      required: true,
    },
  },
  data() {
    return {
      projectId: null,
      hasFindingFetchError: false,
      errorMessage: '',
      isUpdatingFindingState: false,
      isCommentingOnDismissal: false,
      isCreatingMergeRequest: false,
      isCreatingIssue: false,
      isShowingDeleteButtons: false,
      dismissalComment: '',
      finding: null,
    };
  },
  apollo: {
    finding: {
      query: securityReportFindingQuery,
      fetchPolicy: fetchPolicies.NETWORK_ONLY,
      variables() {
        return this.queryVariables;
      },
      update(data) {
        return data.project?.pipeline?.securityReportFinding;
      },
      result({ data = {} }) {
        const { project } = data;
        const stateComment = project?.pipeline?.securityReportFinding?.stateComment;

        if (project) {
          this.projectId = project.id;
        }

        // if there is an existing comment then this will show it in the text area for editing
        if (stateComment) {
          this.dismissalComment = stateComment;
        }
      },
      error() {
        this.errorMessage = this.$options.i18n.fetchErrorMessage;
      },
    },
  },
  computed: {
    queryVariables() {
      const { projectFullPath, pipelineIid, findingUuid } = this;

      return {
        projectFullPath,
        pipelineIid,
        findingUuid,
      };
    },
    remediation() {
      return this.finding?.remediations?.[0];
    },
    issueLinks() {
      return this.finding?.issueLinks?.nodes || [];
    },
    isFindingResolved() {
      return this.finding?.state === STATE_RESOLVED;
    },
    canDownloadPatch() {
      return this.canCreateMergeRequest && this.remediation?.diff?.length > 0;
    },
    canCreateMergeRequest() {
      return !this.isFindingResolved && !this.mergeRequest && this.remediation;
    },
    canCreateIssue() {
      return (
        this.issueLinks.length < 1 &&
        this.finding?.vulnerability?.userPermissions?.createVulnerabilityFeedback
      );
    },
    showErrorMessage() {
      return !this.isLoading && (Boolean(this.errorMessage) || !this.finding);
    },
    isLoading() {
      return this.$apollo.queries.finding.loading;
    },
    isFindingDismissed() {
      return this.finding?.state === STATE_DISMISSED;
    },
    toggledState() {
      return this.isFindingDismissed ? STATE_DETECTED : STATE_DISMISSED;
    },
    toggledMutationConfig() {
      const mutationConfigsKey = this.isFindingDismissed ? 'revertToDetected' : 'dismiss';
      return this.mutationConfigs[mutationConfigsKey];
    },
    toggledFindingStateMutationConfig() {
      return { ...this.toggledMutationConfig, toggledState: this.toggledState };
    },
    currentUser() {
      const {
        current_user_id: id,
        current_username: username,
        current_user_fullname: name,
      } = window.gon;

      return {
        id,
        name,
        username,
        state: 'active',
      };
    },
    dismissButtonText() {
      const {
        isFindingDismissed,
        isCommentingOnDismissal,
        $options: { i18n },
      } = this;

      if (isFindingDismissed) {
        return i18n.revertDismissFinding;
      }

      if (isCommentingOnDismissal) {
        return i18n.addCommentAndDismiss;
      }

      return i18n.dismissFinding;
    },
    hasExistingDismissalComment() {
      return Boolean(this.finding?.stateComment);
    },
    dismissalProject() {
      const { name: value, webUrl: url } = this.finding.project;
      return {
        value,
        url,
      };
    },
    dismissalFeedback() {
      if (!this.finding.dismissedAt) {
        return null;
      }

      const { dismissedAt, dismissedBy, stateComment: comment } = this.finding;
      const { id, name, username, webUrl } = dismissedBy;

      const author = {
        id,
        name,
        username,
        webUrl,
      };

      const commentDetails = comment ? { comment_author: author, comment } : null;

      return {
        created_at: dismissedAt,
        author,
        comment_details: commentDetails,
      };
    },
    mutationConfigs() {
      const { i18n } = this.$options;
      return {
        dismiss: {
          mutationName: 'securityFindingDismiss',
          mutation: dismissFindingMutation,
          errorMessage: i18n.dismissErrorMessage,
        },
        revertToDetected: {
          mutationName: 'securityFindingRevertToDetected',
          mutation: revertFindingToDetectedMutation,
          errorMessage: i18n.revertDismissErrorMessage,
        },
      };
    },
    footerActionButtons() {
      const buttons = [];

      const MRButton = {
        name: s__('ciReport|Resolve with merge request'),
        tagline: s__('ciReport|Automatically apply the patch in a new branch'),
        loading: this.isCreatingMergeRequest,
        handler: this.createMergeRequest,
        action: 'create-merge-request',
      };
      const downloadButton = {
        name: s__('SecurityReports|Download patch to resolve'),
        tagline: s__('SecurityReports|Download the patch to apply it manually'),
        handler: this.downloadPatch,
        action: 'download-patch',
      };
      const createIssueButton = {
        name: s__('SecurityReports|Create Issue'),
        tagline: s__('SecurityReports|Investigate this vulnerability by creating an issue'),
        loading: this.isCreatingIssue,
        handler: this.createIssue,
        action: 'create-issue',
      };

      if (this.canCreateMergeRequest) {
        buttons.push(MRButton);
      }

      if (this.canDownloadPatch) {
        buttons.push(downloadButton);
      }

      if (this.canCreateIssue) {
        buttons.push(createIssueButton);
      }

      return buttons;
    },
    mergeRequest() {
      return this.finding?.vulnerability.mergeRequest;
    },
  },
  methods: {
    handleCancel() {
      if (this.isCommentingOnDismissal) {
        this.isCommentingOnDismissal = false;
      } else {
        this.closeModal();
      }
    },
    handleCommentEsc() {
      this.isCommentingOnDismissal = false;
      // we need to set the focus on the modal so any subsequent esc key presses can get handled by it
      this.$refs.modal.setFocus();
    },
    closeModal() {
      this.$refs.modal.hide();
    },
    toggleFindingState() {
      return this.mutateFinding(this.toggledFindingStateMutationConfig);
    },
    deleteDismissalComment() {
      this.dismissalComment = '';
      return this.mutateFinding(this.mutationConfigs.dismiss);
    },
    updateDismissalComment() {
      return this.mutateFinding(this.mutationConfigs.dismiss);
    },
    async mutateFinding({ mutation, mutationName, errorMessage, toggledState }) {
      this.isUpdatingFindingState = true;

      try {
        await this.$apollo.mutate({
          mutation,
          refetchQueries: [securityReportFindingQuery],
          variables: {
            uuid: this.findingUuid,
            comment: this.dismissalComment,
          },
          update: (store, response) => {
            const { errors } = response.data[mutationName];

            if (errors.length > 0) {
              throw new Error(errors[0]);
            }

            updateFindingState({
              state: toggledState || this.finding.state,
              store,
              query: securityReportFindingQuery,
              variables: this.queryVariables,
            });

            this.$emit('state-updated', toggledState);
            this.closeModal();
          },
        });
      } catch {
        this.errorMessage = errorMessage;
      } finally {
        this.isUpdatingFindingState = false;
      }
    },
    async createMergeRequest() {
      try {
        this.isCreatingMergeRequest = true;

        const { data } = await this.$apollo.mutate({
          mutation: createMergeRequestMutation,
          variables: {
            uuid: this.findingUuid,
          },
        });

        const { errors, mergeRequest } = data.securityFindingCreateMergeRequest;

        if (errors.length > 0) {
          throw new Error(errors[0]);
        }

        visitUrl(mergeRequest.webUrl);
      } catch {
        this.isCreatingMergeRequest = false;
        this.errorMessage = s__(
          'SecurityReports|There was an error creating the merge request. Please try again.',
        );
      }
    },
    async createIssue() {
      try {
        this.isCreatingIssue = true;

        const { data } = await this.$apollo.mutate({
          mutation: createIssueMutation,
          variables: {
            findingUuid: this.findingUuid,
            projectId: this.projectId,
          },
        });

        const { errors, issue } = data.securityFindingCreateIssue;

        if (errors.length > 0) {
          throw new Error(errors[0]);
        }

        visitUrl(issue.webUrl);
      } catch {
        this.isCreatingIssue = false;
        this.errorMessage = s__(
          'SecurityReports|There was an error creating the issue. Please try again.',
        );
      }
    },
    downloadPatch() {
      download({ fileData: this.remediation.diff, fileName: 'remediation.patch' });
    },
  },
  i18n: {
    fetchErrorTitle: __('Error'),
    fetchErrorMessage: s__(
      'SecurityReports|There was an error fetching the finding. Please try again.',
    ),
    dismissErrorMessage: s__(
      'SecurityReports|There was an error dismissing the finding. Please try again.',
    ),
    revertDismissErrorMessage: s__('SecurityReports|There was an error reverting the dismissal.'),
    cancel: __('Cancel'),
    saveComment: s__('SecurityReports|Save comment'),
    dismissFinding: s__('SecurityReports|Dismiss vulnerability'),
    revertDismissFinding: s__('SecurityReports|Undo dismiss'),
    addCommentAndDismiss: s__('SecurityReports|Add comment & dismiss'),
    dismissCommentPlaceholder: s__('SecurityReports|Add a comment or reason for dismissal'),
  },
  VULNERABILITY_MODAL_ID,
};
</script>

<template>
  <gl-modal
    ref="modal"
    size="lg"
    :visible="true"
    content-class="gl-p-5"
    modal-id="security-finding-modal"
    @hidden="$emit('hidden')"
  >
    <template #modal-header>
      <gl-skeleton-loader
        v-if="isLoading"
        data-testid="title-loading-indicator"
        :width="400"
        :lines="1"
      />
      <h2 v-else class="gl-m-0 gl-font-lg">
        {{ finding ? finding.title : $options.i18n.fetchErrorTitle }}
      </h2>
    </template>

    <div v-if="isLoading" data-testid="content-loading-indicator">
      <!-- this is just a placeholder until the designs have been finalized. Captured in: https://gitlab.com/gitlab-org/gitlab/-/issues/377217 -->
      <!-- the skeleton loaders will also be moved into a separate component so they can be re-used -->
      <gl-skeleton-loader :width="80" :lines="1" />
      <gl-skeleton-loader :width="200" :lines="1" />
    </div>

    <template v-else-if="finding">
      <vulnerability-details-graphql v-bind="finding" />

      <solution-card
        :solution="finding.solution"
        :remediation="remediation"
        :merge-request="mergeRequest"
      />

      <issue-note :issue-links="issueLinks" :project="finding.project" />
      <merge-request-note :merge-request="mergeRequest" :project="finding.project" />

      <gl-card
        v-if="isFindingDismissed || isCommentingOnDismissal"
        data-testid="dismissal-comment-section"
      >
        <dismissal-note
          v-if="dismissalFeedback"
          :feedback="dismissalFeedback"
          :is-commenting-on-dismissal="isCommentingOnDismissal"
          :is-showing-delete-buttons="isShowingDeleteButtons"
          :project="dismissalProject"
          :show-dismissal-comment-actions="!hasExistingDismissalComment || !isCommentingOnDismissal"
          @editVulnerabilityDismissalComment="isCommentingOnDismissal = true"
          @showDismissalDeleteButtons="isShowingDeleteButtons = true"
          @hideDismissalDeleteButtons="isShowingDeleteButtons = false"
          @deleteDismissalComment="deleteDismissalComment"
        />
        <event-item
          v-if="!dismissalFeedback && isCommentingOnDismissal"
          :author="currentUser"
          icon-name="cancel"
          icon-class="ci-status-icon-pending"
        />
        <div
          v-if="isCommentingOnDismissal || !hasExistingDismissalComment"
          class="gl-mt-5 gl-pt-5 gl-border-t"
        >
          <gl-form-textarea
            v-model="dismissalComment"
            :autofocus="isCommentingOnDismissal"
            :placeholder="$options.i18n.dismissCommentPlaceholder"
            @focus="isCommentingOnDismissal = true"
            @keydown.meta.enter="toggleFindingState"
            @keydown.esc.stop.prevent="handleCommentEsc"
          />
        </div>
      </gl-card>
    </template>

    <gl-alert v-if="showErrorMessage" variant="danger" :dismissible="false">
      {{ errorMessage || $options.i18n.fetchErrorMessage }}
    </gl-alert>

    <template #modal-footer>
      <div data-testid="footer">
        <template v-if="isFindingDismissed && isCommentingOnDismissal">
          <gl-button data-testid="cancel-editing-comment" @click="isCommentingOnDismissal = false">
            {{ $options.i18n.cancel }}
          </gl-button>
          <gl-button data-testid="save-edited-comment" @click="updateDismissalComment">
            {{ $options.i18n.saveComment }}
          </gl-button>
        </template>
        <template v-else>
          <gl-button data-testid="cancel-button" @click="handleCancel">
            {{ $options.i18n.cancel }}
          </gl-button>
          <gl-button-group>
            <gl-button
              data-testid="dismiss-button"
              :loading="isUpdatingFindingState"
              @click="toggleFindingState"
            >
              {{ dismissButtonText }}
            </gl-button>
            <gl-button
              v-if="!isFindingDismissed && !isCommentingOnDismissal"
              v-gl-tooltip
              data-testid="dismiss-with-comment-button"
              icon="comment"
              :title="$options.i18n.addCommentAndDismiss"
              :aria-label="$options.i18n.addCommentAndDismiss"
              @click="isCommentingOnDismissal = true"
            />
          </gl-button-group>
        </template>
        <split-button
          v-if="footerActionButtons.length > 1"
          :buttons="footerActionButtons"
          data-testid="footer-action-buttons"
          data-qa-selector="resolve_split_button"
          @create-merge-request="createMergeRequest"
          @download-patch="downloadPatch"
        />
        <gl-button
          v-if="footerActionButtons.length === 1"
          :data-testid="`${footerActionButtons[0].action}-button`"
          :loading="isCreatingIssue"
          variant="confirm"
          category="secondary"
          @click="footerActionButtons[0].handler"
        >
          {{ footerActionButtons[0].name }}
        </gl-button>
      </div>
    </template>
  </gl-modal>
</template>
